Micron Document
🎖️GitЯра🎖️

Commit c4b47d80abea194d32973eaa981cafdafc4ddec1


Parents : ca046ed
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-27T16:09:19-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-27T16:09:19-05:00

fix(widget): remove Glance's crash-on-launch ActionTrampolineActivity (#6467)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index 45cd2a4918..8fdb99f01a 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -316,6 +316,33 @@
android:resource="@xml/widget_local_stats_info" />
</receiver>
+ <!--
+ glance-appwidget declares ActionTrampolineActivity, but nothing in Glance ever targets it. Its onCreate
+ calls launchTrampolineAction(intent), which require()s an "ACTION_INTENT" Intent extra, so ANY launch that
+ does not carry that extra is a guaranteed FATAL IllegalArgumentException ("List adapter activity trampoline
+ invoked without specifying target intent") before a single line of our code runs.
+
+ Nothing can supply that extra, because nothing creates the intent. Verified against the Glance sources for
+ our pinned 1.2.0-rc01 (and 1.3.0-alpha02): applyTrampolineIntent() picks ActionTrampolineActivity only for
+ ActionTrampolineType.ACTIVITY, and no call site ever passes ACTIVITY. The four call sites all live in
+ getFillInIntentForAction() - i.e. only inside a lazy collection - and pass BROADCAST / SERVICE /
+ FOREGROUND_SERVICE, which all route to InvisibleActionTrampolineActivity instead. StartActivityAction is
+ special-cased there to skip the trampoline entirely and ride the collection's pending-intent template. The
+ non-lazy path never trampolines at all: actionStartActivity() becomes a direct PendingIntent.getActivity()
+ and actionRunCallback() a direct PendingIntent.getBroadcast() to ActionCallbackBroadcastReceiver.
+
+ So this is dead-but-launchable library code: an exported=false activity that crashes the process on every
+ launch and that our widget can never legitimately reach (LocalStatsWidget has no lazy collection, and a tap
+ on it dispatches its own PendingIntent directly). Same defect class as the ATAK marker below. Removing the
+ node is the whole fix - it takes the component out of the merged manifest so it cannot be started.
+
+ If glance is ever bumped, re-verify that ActionTrampolineType.ACTIVITY still has no producer before
+ trusting this removal; GlanceTrampolineManifestTest guards the removal itself.
+ -->
+ <activity
+ android:name="androidx.glance.appwidget.action.ActionTrampolineActivity"
+ tools:node="remove" />
+
<!--
ATAK plugin-discovery marker. The action is what ATAK looks for, so the filter stays exported; it now
resolves to a real no-op activity because the name used to be com.atakmap.app.component, a class not

diff --git a/androidApp/src/test/kotlin/org/meshtastic/app/widget/GlanceTrampolineManifestTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/widget/GlanceTrampolineManifestTest.kt
new file mode 100644
index 0000000000..faecac8bd1
--- /dev/null
+++ b/androidApp/src/test/kotlin/org/meshtastic/app/widget/GlanceTrampolineManifestTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.app.widget
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.test.core.app.ApplicationProvider
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+
+/**
+ * Guards the `tools:node="remove"` of `androidx.glance.appwidget.action.ActionTrampolineActivity` in
+ * `androidApp/src/main/AndroidManifest.xml`.
+ *
+ * That activity's `onCreate` unconditionally `require()`s an `"ACTION_INTENT"` Intent extra, so any launch without it
+ * is a FATAL `IllegalArgumentException` ("List adapter activity trampoline invoked without specifying target intent")
+ * raised before any of our code runs. Nothing can supply the extra, because nothing in Glance ever creates the intent:
+ * `applyTrampolineIntent()` selects this component only for `ActionTrampolineType.ACTIVITY`, and no call site passes
+ * `ACTIVITY` — the lazy-collection call sites all pass `BROADCAST`/`SERVICE`/`FOREGROUND_SERVICE`, which route to
+ * [androidx.glance.appwidget.action.InvisibleActionTrampolineActivity] instead. It is dead-but-launchable library code,
+ * so we take it out of the merged manifest entirely.
+ *
+ * These assertions run against the *merged* manifest (Robolectric reads it, which is why the AAR-declared invisible
+ * trampoline resolves), so they fail if the removal is dropped or if a Glance bump reshuffles these components.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [34])
+class GlanceTrampolineManifestTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun `crash-on-launch activity trampoline is absent from the merged manifest`() {
+ assertFailsWith<PackageManager.NameNotFoundException>(
+ "$ACTION_TRAMPOLINE is declared in the merged manifest. Glance never targets it, and its onCreate " +
+ "require()s an ACTION_INTENT extra, so every launch is a FATAL IllegalArgumentException. Restore " +
+ "the tools:node=\"remove\" entry in androidApp/src/main/AndroidManifest.xml.",
+ ) {
+ context.packageManager.getActivityInfo(ComponentName(context, ACTION_TRAMPOLINE), 0)
+ }
+ }
+
+ @Test
+ fun `invisible trampoline Glance actually uses is still declared and not exported`() {
+ // Positive control: proves the merged manifest under test really does include glance-appwidget's own
+ // components, so the assertion above is a genuine absence rather than a manifest that was never merged.
+ val activityInfo = context.packageManager.getActivityInfo(ComponentName(context, INVISIBLE_TRAMPOLINE), 0)
+
+ assertFalse(
+ activityInfo.exported,
+ "$INVISIBLE_TRAMPOLINE must stay unexported — it launches intents handed to it verbatim.",
+ )
+ }
+
+ private companion object {
+ const val ACTION_TRAMPOLINE = "androidx.glance.appwidget.action.ActionTrampolineActivity"
+ const val INVISIBLE_TRAMPOLINE = "androidx.glance.appwidget.action.InvisibleActionTrampolineActivity"
+ }
+}

Served by rngit 1.5.4 - Generated in 0.12s